/*
This file is part of La Linea.

La Linea is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

La Linea is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with La Linea.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "../GameEngine/globals.h"

#include <SDL/SDL.h>

#include <sstream>
#include <iostream>
#include <vector>

#include "../GameEngine/Animation.h"
#include "../GameEngine/CacheManager.h"
#include "../GameEngine/globals.h"
#include "../GameEngine/Frame.h"
#include "../GameEngine/Font.h"
#include "../GameEngine/FontManager.h"

// Default screen width/Height
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600

using namespace std;

// Viewport (Re)initialization
void resizeViewport(int width, int height){
	glViewport(0,0,(GLsizei) width, (GLsizei) height);
	glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    gluOrtho2D(0,((GLfloat)width/height),0,1);
	glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

// Video (Re)initialization
void setVideo(int width, int height, unsigned int flags){
	SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS,1);
	SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES,6);
	SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);

	if(!SDL_SetVideoMode(width, height, 32, SDL_OPENGL | SDL_RESIZABLE)){
		cerr << "Erreur lors de l'appel a SetVideoMode" << endl;
	}

	// glewInit
	GLenum err = glewInit();
	if (GLEW_OK != err)
	{
	  fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
	  exit(1);
	}

	// Alpha canal
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	// Enable or disable antialiasing
	if((flags & VIDEO_ANTIALIASING) == VIDEO_ANTIALIASING) glEnable(GL_MULTISAMPLE);
	else glDisable(GL_MULTISAMPLE);

	resizeViewport(width, height);
}

// Display a grid
void displayGrid(){

	glBegin(GL_LINES);

	for(int i=-20; i<20; i++){
			if(i==0)
				glColor3f(1.0, 1.0, 1.0);
			else
				glColor4f(0.5, 0.5, 0.5, 0.5);

			glVertex3f(-2,(GLfloat)i/10,1);
			glVertex3f(2,(GLfloat)i/10,1);
	}

	for(int i=-20; i<20; i++){
			if(i==0)
				glColor3f(1.0, 1.0, 1.0);
			else
				glColor4f(0.5, 0.5, 0.5, 0.5);

			glVertex3f((GLfloat)i/10,-2,1);
			glVertex3f((GLfloat)i/10,2,1);
	}


	glEnd();

}

// Main :o)
int main(int argc, char *argv[])
{
	#ifdef _FINDLEAKS
		_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
	#endif

	// Various variables
	unsigned int displayOptions = DRAW_CONTROLPOINTS | DRAW_ORIGINPOINTS | DRAW_BORDERLINE | DRAW_GHOST | DRAW_GRID;
	unsigned int videoOptions = VIDEO_ANTIALIASING;

	bool quit = false; // Variable used for the rendering loop
	bool dragAndDrop = false; // True if we're in drag & drop mode
	bool play = false;
	Uint32 time = SDL_GetTicks();
	Uint32 fpsCounterTime = time;
	int fpsCounter = 0;
	int fps = 0;

	int screenWidth = SCREEN_WIDTH;
	int screenHeight = SCREEN_HEIGHT;
	GLfloat screenFormat = (GLfloat)SCREEN_WIDTH/SCREEN_HEIGHT;

	// Scene translation vector. We place the (0,0) at the middle of the x axis, and
	// a bit above the bottom of the screen.
	Vector2f translateScene = Vector2f(screenFormat/2,(GLfloat)0.10);
	GLfloat zoom = 1.0;

	Node *selectedNode = 0; // Represent the selected node


	/// INITIALIZATION

    SDL_Init(SDL_INIT_VIDEO); // Intialize SDL
    SDL_WM_SetCaption("La Linea - Animation Editor",NULL); // Window's title
	setVideo(screenWidth, screenHeight, videoOptions); // Intialize video
	CacheManager::getInstance()->init(); // Initialize frame cache

	// Set the background color
	glClearColor((GLclampf)0.0,(GLclampf)0.39,(GLclampf)0.14,(GLclampf)1.0);

	/*
		If the app was called with an argument, we assume this is an
		animation file. Otherwhise we create a new one.
	*/
	Animation* a = new Animation("AnimationTest");
	if(argc > 1){
		a->loadAnim(argv[1]);
	}
	else{
		Frame* f = new Frame();
		a->addFrame(f);
	}
	
	Font* font = FontManager::getInstance()->getFont("arial.ttf", 18);

	SDL_Event event;

	// Rendering loop
	while(!quit){
		// While there is events to handle
		while (SDL_PollEvent(&event))
		{

			// Mouse position
			Vector2f posSouris = Vector2f((((GLfloat)event.motion.x*screenFormat)/screenWidth), 1-(GLfloat)event.motion.y/screenHeight);
			posSouris.x -= translateScene.x;
			posSouris.y -= translateScene.y;

			posSouris.x /= zoom;
			posSouris.y /= zoom;

			switch(event.type)
			{
				case SDL_QUIT:
					quit = true; // We leave the loop
					break;

				case SDL_VIDEORESIZE:
					screenWidth = event.resize.w;
					screenHeight = event.resize.h;
					screenFormat = (GLfloat)screenWidth/screenHeight;
					setVideo(screenWidth, screenHeight, videoOptions);
					break;

				case SDL_KEYDOWN:
					switch(event.key.keysym.sym){
						// Display control
						case SDLK_1: // 1 -> Control points display
							displayOptions = displayOptions ^ DRAW_CONTROLPOINTS;
							displayOptions = displayOptions ^ DRAW_ORIGINPOINTS;
							break;
						case SDLK_2: // 2 -> Curve points display
							displayOptions = displayOptions ^ DRAW_CURVEPOINTS;
							break;
						case SDLK_3: // 3 -> Curve display
							displayOptions = displayOptions ^ DRAW_CURVELINE;
							break;
						case SDLK_4: // 4 -> Border points display
							displayOptions = displayOptions ^ DRAW_BORDERPOINTS;
							break;
						case SDLK_5: // 5 -> Final render display
							displayOptions = displayOptions ^ DRAW_BORDERLINE;
							break;
						case SDLK_g: // G -> Display/Hide the grid
							displayOptions = displayOptions ^ DRAW_GRID;
							break;
						case SDLK_h: // H-> Display/Hide the animation ghost
							displayOptions = displayOptions ^ DRAW_GHOST;
							break;
						case SDLK_b: // B-> Display/Hide bounding box
							displayOptions = displayOptions ^ DRAW_BOUNDINGBOX;
							break;

						// Image quality control
						case SDLK_q: // Q -> Antialiasing on/off
							videoOptions = videoOptions ^ VIDEO_ANTIALIASING;
							setVideo(screenWidth, screenHeight, videoOptions);
							break;
						case SDLK_w: // W -> Lower the quality
							quality = quality>1?quality-1:quality;
							a->invalidateFrames();
							break;
						case SDLK_e: // E -> Improve the quality
							quality++;
							a->invalidateFrames();
							break;

						// Other commands
						case SDLK_s: // S -> Save
							a->saveAnim("AnimationTest.anim");
							break;
						case SDLK_o: // O -> Open
							a->loadAnim("AnimationTest.anim");
							break;
						case SDLK_n: // N -> New frame (replace the current one)
							selectedNode = 0;
							a->replaceCurrentFrame(new Frame());
							break;
						case SDLK_d: // D -> Unselect
							selectedNode = 0;
							break;
						case SDLK_DELETE: // Delete -> Delete the selected node
							if(selectedNode) a->getCurrentFrame()->deleteNode(selectedNode);
							selectedNode = 0;
							CacheManager::getInstance()->getFrameCache()->invalidate(a->getCurrentFrame());
							break;
						case SDLK_i: // I -> Insert a new frame
							a->addFrame(new Frame());
							break;
						case SDLK_LEFT: // Left arrow -> Previous frame
							selectedNode = 0;
							a->previousFrame();
							break;
						case SDLK_RIGHT: // Right arrow -> Next frame
							selectedNode = 0;
							a->nextFrame();
							break;
						case SDLK_UP: // Up arrow -> Increase the frame duration
							a->setFrameDuration(a->getFrameDuration()+0.01f);
							break;
						case SDLK_DOWN: // Down arrow -> Diminish the frame duration
							if(a->getFrameDuration() > 0.02)
								a->setFrameDuration(a->getFrameDuration()-0.01f);
							break;
						case SDLK_p: // P -> Play/Stop animation
							play = !play;
							break;
                        default:
                            break;
					}

					break;

				case SDL_MOUSEMOTION:
					// If a node is selected and we're in drag & drop mode, we move it
					if(dragAndDrop && selectedNode){
						selectedNode->_pos.x = posSouris.x;
						selectedNode->_pos.y = posSouris.y;
						displayOptions = displayOptions | DRAW_NOCACHE;
					}
					// If no node is selected and we're in drag & drop mode, we move the scene
					else if (dragAndDrop){
						translateScene.x = translateScene.x + ((GLfloat)event.motion.xrel*screenFormat/screenWidth);
						translateScene.y = translateScene.y - ((GLfloat)event.motion.yrel/(GLfloat)screenHeight);
					}
					break;

				case SDL_MOUSEBUTTONUP:
					// If a mouse button is release in drag & drop mode, we leave it
					if(dragAndDrop){
						if(selectedNode) CacheManager::getInstance()->getFrameCache()->invalidate(a->getCurrentFrame());
						dragAndDrop = false;
						displayOptions = displayOptions & ~DRAW_NOCACHE;
					}
					break;

				case SDL_MOUSEBUTTONDOWN:
					// If the left mouse button is pressed
					if(event.button.button == SDL_BUTTON_LEFT){
						bool isNearPoint = false;
						Node* nearestPoint = 0;
						selectedNode = 0;

						// We look for the nearest point to the cursor
						vector<OneLineCurve*> lines = a->getCurrentFrame()->getLines();
						for(vector<OneLineCurve*>::iterator itOLC = lines.begin(); itOLC != lines.end(); ++itOLC){
							std::deque<Curve*> curves = (*itOLC)->getCurves();
							for(deque<Curve*>::iterator itCurves = curves.begin(); itCurves != curves.end(); ++itCurves){
								if(!nearestPoint) nearestPoint = (*itCurves)->_origins[0];

								if((posSouris-(*itCurves)->_origins[0]->_pos).length() <= (posSouris-nearestPoint->_pos).length()){
									nearestPoint = (*itCurves)->_origins[0];
								}
								if((posSouris-(*itCurves)->_origins[1]->_pos).length() <= (posSouris-nearestPoint->_pos).length()){
									nearestPoint = (*itCurves)->_origins[1];
								}
								if((posSouris-(*itCurves)->_controlpoint->_pos).length() <= (posSouris-nearestPoint->_pos).length()){
									nearestPoint = (*itCurves)->_controlpoint;
								}
							}
						}
						// If there's a point for which the distance to the cursor is less than 0.02, we select
						// it and go to drag & drop mode
						if(nearestPoint && !dragAndDrop && (posSouris-nearestPoint->_pos).length() < 0.02){
							isNearPoint = true;
							dragAndDrop = true;
							selectedNode = nearestPoint;
						}
					}
					else if(event.button.button == SDL_BUTTON_RIGHT){

						// If there's a selected point, we try to find from which OneLineCurve he comes
						// from. Then, we check if it is the first or the last point in that curve
						// (impossible to add another curve if this is not the case)
						if(selectedNode){
							OneLineCurve *selectedLine = 0;

							for(unsigned int i=0; i< a->getCurrentFrame()->getLines().size(); i++){
								std::deque<Curve*> curves = a->getCurrentFrame()->getLines()[i]->getCurves();
								std::deque<Curve*>::iterator firstpoint = curves.begin();
								std::deque<Curve*>::iterator lastpoint = curves.end()-1;

								// If this is the first point
								if((*firstpoint)->_origins[0] == selectedNode){
									selectedLine = a->getCurrentFrame()->getLines()[i];
									Node* newNode = new Node(posSouris, selectedNode->_size);
									Curve *newCurve = new Curve();

									newCurve->_origins[0] = newNode;
									newCurve->_origins[1] = selectedNode;
									newCurve->_controlpoint = new Node(selectedNode->_pos+((newNode->_pos-selectedNode->_pos)/2),0.0);
									selectedLine->addCurve(newCurve);
									selectedNode = newNode;
								}
								// If this is the last point
								else  if ((*lastpoint)->_origins[1] == selectedNode){
									selectedLine = a->getCurrentFrame()->getLines()[i];
									Node* newNode = new Node(posSouris, selectedNode->_size);
									Curve *newCurve = new Curve();

									newCurve->_origins[0] = selectedNode;
									newCurve->_origins[1] = newNode;
									newCurve->_controlpoint = new Node(selectedNode->_pos+((newNode->_pos-selectedNode->_pos)/2),0.0);
									selectedLine->addCurve(newCurve);
									selectedNode = newNode;
								}
							}
						}
						// If no point is selected, we create a new curve
						else{
							Node* newNode = new Node(posSouris, 0.02);
							OneLineCurve *curves = new OneLineCurve();
							Curve *newCurve = new Curve();
							newCurve->_origins[0] = newNode;
							newCurve->_origins[1] = new Node(*newNode);
							newCurve->_controlpoint = new Node(*newNode);
							curves->addCurve(newCurve);
							a->getCurrentFrame()->addLine(curves);
						}

						CacheManager::getInstance()->getFrameCache()->invalidate(a->getCurrentFrame());
					}

					// If the mousewheel was scrolled up, we increase the size of the
					// selected node if there's one. Otherwhise, we zoom.
					else if(event.button.button == SDL_BUTTON_WHEELUP){
						if(selectedNode){
							selectedNode->_size += 0.005;
							CacheManager::getInstance()->getFrameCache()->invalidate(a->getCurrentFrame());
						}
						else
							zoom+=(GLfloat)0.03;
					}

					// If the mousewheel was scrolled down, we diminish the size of the
					// selected node if there's one (without going under 0). Otherwhise we
					// unzoom.
					else if(event.button.button == SDL_BUTTON_WHEELDOWN){
						if(selectedNode){
							selectedNode->_size = (selectedNode->_size > 0.005)?selectedNode->_size-0.005:0.0;
							CacheManager::getInstance()->getFrameCache()->invalidate(a->getCurrentFrame());
						}
						else
							zoom = (zoom > 0.03)?zoom - (GLfloat)0.03:(GLfloat)0.03;
					}

					// If there's a mousewheel click
					else if(event.button.button == SDL_BUTTON_MIDDLE){
						if(!dragAndDrop){
							dragAndDrop = true;
							selectedNode = 0;
						}
					}

					break;
			}
		}
		// Clear the screen
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();

		// Translate and zoom in the scene
		glTranslatef(translateScene.x, translateScene.y, (GLfloat)0.0);
		glScalef(zoom, zoom, 0);

		// Display the grid if it is activated
		if((displayOptions & DRAW_GRID) == DRAW_GRID) displayGrid();

		// Display the current frame
		a->draw(1.0, 1.0, 1.0, 1.0, displayOptions);

		Uint32 newTime = SDL_GetTicks();

		// FPS calculation
		fpsCounter++;
		if((newTime - fpsCounterTime) > 1000){
			fps = fpsCounter;
			fpsCounter = 0;
			fpsCounterTime = newTime;
		}

		// Update the animation if we're in play mode
		if(play)
			a->update((newTime-time)/(float)1000);
		time = newTime;

		// Display the selected node
		if(selectedNode){
			glPointSize(7.0);
			glColor3f(1.0, 0.0, 0.0);
			glBegin(GL_POINTS);
				glVertex2f(selectedNode->_pos.x, selectedNode->_pos.y);
			glEnd();
		}

		// Display various informations
		ostringstream oss;
		oss << "FPS : " << fps << endl;
		oss << "Frame : "<<a->getCurrentFrameNumber()+1<<"/"<<a->getFrameNumber()<<endl;
		oss << "Frame duration : "<<a->getFrameDuration()<<" seconds"<<endl;
		glColor3f(1.0,1.0,1.0);
		FontManager::write(*font, oss.str(), Vector2f(10.0,(GLfloat)screenHeight-30));

		// Flush + Swap
		glFlush();
		SDL_GL_SwapBuffers();
	}

	// Delete the animation from the memory
	delete a;
	
	CacheManager::destroy();
	FontManager::destroy();
    return 0;
}
